package cz.fi.muni.pb138.spring2013.dtd2xsd.visitor.implementation;

import cz.fi.muni.pb138.spring2013.dtd2xsd.entities.Attribute;
import cz.fi.muni.pb138.spring2013.dtd2xsd.entities.AttributeType;
import cz.fi.muni.pb138.spring2013.dtd2xsd.entities.ChoiceRestriction;
import cz.fi.muni.pb138.spring2013.dtd2xsd.entities.Document;
import cz.fi.muni.pb138.spring2013.dtd2xsd.entities.ElementRestriction;
import cz.fi.muni.pb138.spring2013.dtd2xsd.entities.SequenceRestriction;
import cz.fi.muni.pb138.spring2013.dtd2xsd.entities.SimpleElementRestriction;
import cz.fi.muni.pb138.spring2013.dtd2xsd.visitor.interfaces.ExtendedContentRestriction;
import cz.fi.muni.pb138.spring2013.dtd2xsd.visitor.interfaces.Visitor;
import java.io.OutputStream;
import java.util.LinkedList;
import java.util.List;
import java.util.Stack;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.w3c.dom.DOMException;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

/**
 * Class implements {@link cz.fi.muni.pb138.spring2013.dtd2xsd.visitor.interfaces.Visitor
 * Visitor}.
 *
 * It builds {@link org.w3c.dom.Document Document} and when the Document is
 * built the class is able to transfer it to readable form.
 *
 * See: {@link XMLSchemaVisitor#getPrintableVersion(java.io.OutputStream)
 * XMLSchemaVisitor.getPrintableVersion(java.io.OutputStream)}
 *
 * @author <a href="mailto:ramikvl at gmail dot com">Vlastislav Ramik</a>
 */
public class XMLSchemaVisitor implements Visitor {

    private org.w3c.dom.Document doc;
    private Element currentElement;
    private List<Element> xsAttributes = new LinkedList<>();
    private Stack<Integer> stack = new Stack<>();

    public XMLSchemaVisitor() throws ParserConfigurationException {
        DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
        DocumentBuilder builder = builderFactory.newDocumentBuilder();
        doc = builder.newDocument();
    }

    @Override
    public void visit(Document document) {
        Element xsSchema = doc.createElementNS("http://www.w3.org/2001/XMLSchema", "xs:schema");
        xsSchema.setAttribute("elementFormDefault", "qualified");

        doc.appendChild(xsSchema);
        currentElement = xsSchema;
    }

    @Override
    public void visit(SequenceRestriction sequenceRestriction) {
        decreaseTopOfTheStack();
        stack.push(sequenceRestriction.getElements().size());
        
        Element xsSequence = doc.createElement("xs:sequence");
        resolveOccurrence(sequenceRestriction, xsSequence);

        if (currentElement.getNodeName().equals("xs:element")) {
            Element xsComplexType = doc.createElement("xs:complexType");
            if (sequenceRestriction.getElements().contains(SimpleElementRestriction.PCDATA)) {
                xsComplexType.setAttribute("mixed", "true");
                decreaseTopOfTheStack();
            }
            xsComplexType.appendChild(xsSequence);
            moveListToXsElement(xsAttributes, xsComplexType);
            currentElement.appendChild(xsComplexType);
        } else {
            currentElement.appendChild(xsSequence);
        }
        
        currentElement = xsSequence;
    }

    @Override
    public void visit(ChoiceRestriction choiceRestriction) {
        decreaseTopOfTheStack();
        stack.push(choiceRestriction.getElements().size());
        
        Element xsChoice = doc.createElement("xs:choice");
        resolveOccurrence(choiceRestriction, xsChoice);

        if (currentElement.getNodeName().equals("xs:element")) {
            Element xsComplexType = doc.createElement("xs:complexType");
            if (choiceRestriction.getElements().contains(SimpleElementRestriction.PCDATA)) {
                xsComplexType.setAttribute("mixed", "true");
                decreaseTopOfTheStack();
            }
            xsComplexType.appendChild(xsChoice);
            currentElement.appendChild(xsComplexType);
        } else {
            currentElement.appendChild(xsChoice);
        }
        
        currentElement = xsChoice;
    }

    @Override
    public void visit(ElementRestriction elementRestriction) {
        Element xsElement = doc.createElement("xs:element");
        xsElement.setAttribute("name", elementRestriction.getName());
        resolveOccurrence(elementRestriction, xsElement);

        decreaseTopOfTheStack();
        
        currentElement.appendChild(xsElement);
        currentElement = xsElement;
    }

    @Override
    public void visit(Attribute attribute) {
        Element xsAttribute = doc.createElement("xs:attribute");
        xsAttribute.setAttribute("name", attribute.getName());

        resolveAttributeType(attribute, xsAttribute);
        resolveAttributeDefaultValue(attribute, xsAttribute);

        xsAttributes.add(xsAttribute);
    }

    @Override
    public void visit(SimpleElementRestriction simpleElementRestriction) {
        if (currentElement.getNodeName().equals("xs:element")) {
            boolean hasAttributes = !xsAttributes.isEmpty();
            Element xsComplexType;
            switch (simpleElementRestriction) {
                case EMPTY:
                    xsComplexType = doc.createElement("xs:complexType");
                    if (hasAttributes) {
                        moveListToXsElement(xsAttributes, xsComplexType);
                    }
                    currentElement.appendChild(xsComplexType);
                    break;
                case PCDATA:
                    if (hasAttributes) {
                        xsComplexType = doc.createElement("xs:complexType");
                        Element xsSimpleContent = doc.createElement("xs:simpleContent");
                        Element xsExtension = doc.createElement("xs:extension");
                        xsExtension.setAttribute("base", "xs:string");

                        xsComplexType.appendChild(xsSimpleContent);
                        xsSimpleContent.appendChild(xsExtension);
                        
                        moveListToXsElement(xsAttributes, xsExtension);
                        
                        currentElement.appendChild(xsComplexType);
                        
                    } else {
                        currentElement.setAttribute("type", "xs:string");
                    }
                    break;
                case ANY:
                    xsComplexType = doc.createElement("xs:complexType");
                    Element xsSequence = doc.createElement("xs:sequence");
                    Element xsAny = doc.createElement("xs:any");

                    xsComplexType.appendChild(xsSequence);
                    xsSequence.appendChild(xsAny);
                    if (hasAttributes) {
                        moveListToXsElement(xsAttributes, xsSequence);
                    }
                    currentElement.appendChild(xsComplexType);
                    break;
                default:
                    System.err.println("Unknown simpleElementRestriction");
            }
            setCurrentElementToLvlUp();
            
            while (!stack.empty() && stack.peek() == 0) {
                stack.pop();

                setCurrentElementToLvlUp();
            }
        }
    }

    /**
     * Method which takes care about transforming the created XML Schema
     * ({@link org.w3c.dom.Document Document}) to readable form.
     *
     * @param outputStream - outputStream where the result will be sent
     * @throws TransformerConfigurationException see:
     * {@link javax.xml.transform.TransformerFactory#newTransformer() TransformerFactory.newTransformer}
     * @throws TransformerException see: null     {@link javax.xml.transform.TransformerFactory#newTransformer() TransformerFactory.newTransformer},
     * {@link javax.xml.transform.Transformer#transform(javax.xml.transform.Source, javax.xml.transform.Result)  Transformer.tranform}
     */
    public void getPrintableVersion(OutputStream outputStream) throws TransformerConfigurationException, TransformerException {
        TransformerFactory factory = TransformerFactory.newInstance();
        Transformer transformer = factory.newTransformer();
        DOMSource source = new DOMSource(doc);
        StreamResult result = new StreamResult(outputStream);
        transformer.setOutputProperty(OutputKeys.INDENT, "yes");
        transformer.transform(source, result);
    }

    /**
     * Method for moving specified list to specified element
     *
     * @param list the list to move
     * @param xsElementToAdd Element where the list will be appended
     * @throws DOMException see:
     * {@link org.w3c.dom.Node#appendChild(org.w3c.dom.Node) Node.appendChild}
     */
    private void moveListToXsElement(List<Element> list, Element xsElementToAdd) throws DOMException {
        for (Element element : list) {
            xsElementToAdd.appendChild(element);
        }
        list.clear();
    }

    /**
     * The method builds xs:attribute element. It resolves attribute
     * default-value. There are four possibilities what value can be
     * default-value: value, #REQUIRED, #IMPLIED, #FIXED value.
     *
     * The method finds out which value is default-value and it builds the
     * xs:attribute according to the value.
     *
     * @param attribute to decide what default-value is the attribute
     * @param xsAttribute xs:attribute which is built
     * @throws DOMException see:
     * {@link org.w3c.dom.Element#setAttribute(String, String) Element#setAttribute}
     */
    private void resolveAttributeDefaultValue(Attribute attribute, Element xsAttribute) throws DOMException {
        if (attribute.isRequired()) { //#REQUIRED
            xsAttribute.setAttribute("use", "required");
        }
        if (attribute.isFixed()) { // #FIXED value
            xsAttribute.setAttribute("fixed", attribute.getDefaultValue());
        } else if (attribute.getDefaultValue() != null) { // value
            xsAttribute.setAttribute("default", attribute.getDefaultValue());
        } //for #IMPLIED: DO NOTHING
    }

    /**
     * The method builds xs:attribute element. It resolves attribute
     * attribute-type. There are few possibilities what value can be
     * attribute-type: see
     * {@link cz.fi.muni.pb138.spring2013.dtd2xsd.entities.AttributeType AttributeType}
     *
     * @param attribute to decide what attribute-type is the attribute
     * @param xsAttribute xs:attribute which is built
     * @throws DOMException see:
     * {@link org.w3c.dom.Element#setAttribute(String, String) Element#setAttribute}
     */
    private void resolveAttributeType(Attribute attribute, Element xsAttribute) throws DOMException {
        AttributeType type = attribute.getType();
        switch (type) {
            case CDATA:
                xsAttribute.setAttribute("type", "xs:string");
                break;
            case ID:
                xsAttribute.setAttribute("type", "xs:ID");
                break;
            case IDREF:
                xsAttribute.setAttribute("type", "xs:IDREF");
                break;
            case IDREFS:
                xsAttribute.setAttribute("type", "xs:IDREFS");
                break;
            case NMTOKEN:
                xsAttribute.setAttribute("type", "xs:NMTOKEN");
                break;
            case NMTOKENS:
                xsAttribute.setAttribute("type", "xs:NMTOKENS");
                break;
            case ENUMERATED:

                Element xsSimpleType = doc.createElement("xs:simpleType");
                Element xsRestriction = doc.createElement("xs:restriction");
                xsRestriction.setAttribute("base", "xs:string");

                xsSimpleType.appendChild(xsRestriction);
                for (String enumValue : attribute.getEnumeratedValues()) {
                    Element xsEnumeration = doc.createElement("xs:enumeration");
                    xsEnumeration.setAttribute("value", enumValue);

                    xsRestriction.appendChild(xsEnumeration);
                }
                xsAttribute.appendChild(xsSimpleType);
                break;
            default:
                System.err.println("Unknown AttributeType");
        }
    }

    /**
     * The method adds minOcccurs and/or maxOccurs to specified Element
     * 
     * @throws DOMException 
     */
    private void resolveOccurrence(ExtendedContentRestriction extendedContentRestriction, Element xsElement) throws DOMException {
        if (!extendedContentRestriction.isRequired()) {
            xsElement.setAttribute("minOccurs", "0");
        }
        if (extendedContentRestriction.isMultiple()) {
            xsElement.setAttribute("maxOccurs", "unbounded");
        }
    }

    /**
     * Sets currtent Element to its parent.
     * 
     * @throws IllegalStateException 
     */
    private void setCurrentElementToLvlUp() throws IllegalStateException {
        Node parentNode = currentElement.getParentNode();
        if (parentNode.getNodeType() == Node.ELEMENT_NODE) {
            currentElement = (Element) parentNode;
        } else {
            throw new IllegalStateException("Parent node is not element");
        }
    }

    /**
     * Decreases the top of the stack of one.
     */
    private void decreaseTopOfTheStack() {
        if (!stack.empty()) {
            int pop = stack.pop().intValue();
            stack.push(--pop);
        }
    }
}
